// const objfn = require('objectfn')
// const addWith = require('with')
// const scopedEval = require('./scoped_eval')
var _ = require('lodash')
// self-closing tags list
const selfClosing = ['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr']

module.exports = function CodeGenerator (tree, options = {}) {
  options.selfClosing = options.selfClosing || 'close'
  options.returnString = options.returnString || false
  options.runtimeName = options.runtimeName || '__runtime'
  options.scopedLocals = options.scopedLocals || false
  options.locals = new Set()

  if (['close', 'tag', 'slash'].indexOf(options.selfClosing) < 0) {
    throw new Error(`'${options.selfClosing}' is an invalid option for 'selfClosing'. You can use 'close', 'tag', or 'slash'`)
  }

  // runtime must be bound with the correct name on eval
  const ctx = {}
  // ctx[options.runtimeName] = this.runtime

  // return either a stringified function, or eval'd function
  let js = `${walk(tree, options)}`
  // js = options.scopedLocals ? `return ${js}` : formatLocals(js, options)
  // const func = `;(function (locals) { ${js} })`
  // // eval is not as good as, but much faster than node/vm
  // return options.returnString ? func : scopedEval(ctx, func)
  return js
}

function walk (tree, options) {
  // coerce any input to an array
  tree = Array.prototype.concat(tree)

  return tree.reduce((m, node) => {
    // text node type
    // if (node.type === 'text') {
    //   m += escape(node.content)
    //   return m
    // }

    // tag node type
    // if (node.type === 'tag') {
    //   // open up the tag
    //   m += `<${node.name}`
    //
    //   // add attributes if present
    //   if (node.attrs && Object.keys(node.attrs).length > 0) {
    //     m += attributes(options, node.attrs)
    //   }
    //
    //   // deal with contents and closing tag if applicable
    //   if (selfClosing.indexOf(node.name) > -1) {
    //     m += {
    //       slash: ' />',
    //       tag: `></${node.name}>`,
    //       close: '>'
    //     }[options.selfClosing]
    //   } else {
    //     m += '>'
    //     if (node.content) { m += walk(node.content, options) }
    //     m += `</${node.name}>`
    //   }
    //
    //   return m
    // }

    // code node type
    // if (node.type === 'code') {
    //   // handle customizable runtime name
    //
    //   // When returning a string, runtime is exposed directly. However,
    //   // when the user is asking for a function back, we need to eval the
    //   // function, and eval binds internal variables when it runs. In order to
    //   // avoid scope pollution, we run this eval inside a clean module with
    //   // variable names that are unlikely to conflict. Since you cannot access
    //   // local variables' scope in node.js, we nest the runtime one more level.
    //
    //   let content = options.returnString ? node.content.replace('__runtime', options.runtimeName) : node.content.replace('__runtime', `__reshapeContext.${options.runtimeName}`)
    //   // handle '__nodes' helper
    //   content = content.replace(/__nodes\[(\d+)]/g, (match, i) => {
    //     return `"${walk(node.nodes[i], options)}"`
    //   })
    //   m += `" + (${content}) + "`
    //   return m
    // }
    //
    if (node.type === 'FunctionDeclaration') {
      var arg = []
      if (node.params.length >= 2) {
        _.each(node.params, function(n){
          arg.push(`${walk(n, options)}`)
        })
        arg = arg.join(', ')
      } else {
        arg = `${walk(node.params, options)}`
      }
      m += `${node.defType.name} ${node.name} (${arg}){\n`
      m += `${walk(node.body)}`
      m += '}\n'
      return m
    }

    if (node.type === 'FunctionExpression') {
      var arg = []
      if (node.params.length >= 2) {
        _.each(node.params, function(n){
          arg.push(`${walk(n, options)}`)
        })
        arg = arg.join(', ')
      } else {
        arg = `${walk(node.params, options)}`
      }
      m += `${node.defType.name} ${node.name} (${arg});\n`
      return m
    }

    if (node.type === 'IncludeDefinition') {
      m += `#include ${node.determine}${node.name}${node.close}\n`
      return m
    }
    if (node.type === 'DefineDefinition') {
      m += `#define ${node.name}\t${walk(node.value)}\n`
      return m
    }

    if (node.type === 'StructDefinition') {
      m += `struct ${node.name} {\n`
      if (node.member) {
        _.each(node.member, function(n){
          m += `\t${walk(n)} ;\n`
        })
      }
      m += `};\n`
      return m
    }
    if (node.type === 'TypeDefStatement') {
      m += `typedef ${node.value}`
      return m
    }
    if (node.type === 'Type') {
      if (node.modifier && node.modifier.length) {
        _.each(node.modifier, function(mm){
          m += `${mm} `
        })
      }
      m += node.name
      return m
    }
    if (node.type === 'PointerArrayType') {
      m += `${walk(node.target)}[${walk(node.value)}]`
      return m
    }
    if (node.type === 'PointerType') {
      m += `*${walk(node.target)}`
      return m
    }
    if (node.type === 'GlobalVariableDeclaration') {
      if (node.defType && node.defType.type === 'PointerArrayType') {
        m += `${walk(node.defType.target)} ${node.name}`
        if(node.defType.value && node.defType.value.type) {
          m +=`[${walk(node.defType.value)}]`
        }else if (node.defType.value){
          m +=`[${node.defType.value}]`
        }
      } else if (node.defType){
        m += `${walk(node.defType)} ${node.name}`
      }

      if (node.value !== undefined && node.value.type) {
        m += ` = ${walk(node.value)}`
      } else if (node.value !== undefined ){
          m += ` = ${node.value}`
      }
      m += `;\n`
      return m
    }

    if (node.type === 'VariableDeclaration') {
      m += `${node.value};\n`
      return m
    }

    if (node.type === 'ExpressionStatement') {

      m += `\t${walk(node.expression)}\n`
      return m
    }

    if (node.type === 'ForStatement') {
      m += `for (${node.init}; ${node.condition}; ${node.step}) {\n`
      m += `${walk(node.body)}\n`
      m += '}'
      return m
    }

    if (node.type === 'DoWhileStatement') {
      m += `do { \n${walk(node.body)}\n}`
      m += `while(${node.condition})`
      return m
    }

    if (node.type === 'WhileStatement') {
      m += `while(${node.condition}){ \n`
      m += `${walk(node.body)}\n}`
      return m
    }
    if (node.type === 'IfStatement') {
      m += `if (${node.condition}) {\n`
      m += `${walk(node.body)}\n`
      m += '}'
      if (node.elseif) {
        m+= `else if(${node.elseifcondition}) {\n${walk(node.elseifbody)}}`
      }
      if (node.else) {
        m+= `else {\n${walk(node.else)}}`
      }
      return m
    }
    if (node.type === 'SwitchStatement') {
      m += `switch (${walk(node.value)}) \n`
      if (node.cases.length) {
        for (var i = 0; i<= node.cases.length -1; i++) {
          arg += `${walk(node.cases[i])}`

        }
      }
      if (node.default) {
        arg += `${walk(node.default)}\n break;\n`
      }
      return m
    }
    if (node.type === 'caseStatement') {
      m += `case ${node.case}:\n`
      m += `${walk(node.body)}\n`
      m += `break;\n`
      return m
    }
    if (node.type === 'defultcaseStatement') {
      m += `default:\n ${walk(node.body)}`
      return m
    }

    if (node.type === 'CommentIfStatement') {
      m += `#if ${node.condition} \n`
      m += `${walk(node.body)}\n`
      if (node.elseif) {
        m+= `#elseif ${node.elseifcondition} \n${walk(node.elseifbody)}\n`
      }
      if (node.else) {
        m+= `#else \n${walk(node.else)}}\n`
      }
      m += "#endif"
      return m
    }

    if (node.type === 'ReturnStatement') {
      m += `return ${node.value};`
      return m
    }

    if (node.type === 'BinaryExpression') {m
      m += `${walk(node.left)}${node.operator}${walk(node.right)}`
      return m
    }

    if (node.type === 'CastExpression') {
      m += `(${walk(node.targetType)})${walk(node.value)}`
      return m
    }

    if (node.type === 'IndexExpression') {
      m += `*${node.value}`
      return m
    }

    if (node.type === 'CallExpression') {
      var arg = ''
      if (node.params[0] !== undefined) {
        for (var i = 0; i<= node.params.length -1; i++) {
          arg += `${walk(node.params[i])}`
          if (node.params.length > 1 && i !==  node.params.length -1) {
            arg += ','
          }
        }
      }
      m += `${walk(node.base)}(${arg});`
      return m
    }

    if (node.type === 'PrefixExpression') {
      m += `${node.operator}${walk(node.value)}`
      return m
    }

    if (node.type === 'SuffixExpression') {
      m += `${walk(node.value)}${node.operator}`
      return m
    }

    if (node.type === 'Literal') {
      var entries = ''
      if (_.isArray(node.value) && node.value[0] !== undefined) {
        for (var i = 0; i<= node.value.length -1; i++) {
          entries += `${walk(node.value[i])}`
          if (node.value.length > 1 && i !==  node.value.length -1) {
            entries += ','
          }
        }
      } else if(node.value !== undefined){
        entries += `${node.value}`
      }
      m += `${entries}`
      return m
    }

    if (node.type === 'Identifier') {
      if (node.modifier) {
        m +=`${walk(node.modifier)} `
      }
      m += node.value
      return m
    }



    if (node.type === 'Definition') {
      m += `${node.defType.name} ${node.name}`
      if (node.value) {
        m += ` = ${walk(node.value)}`
      }
      return m
    }
    // comment node type
    if (node.type === 'comment') {
      m += `<!--${escape(node.content)}-->`
      return m
    }
    // if (node.type === undefined) {
    //   m += `${node}`
    //   return m
    // }

    // if we haven't matched any of the node types, it's an error
    throw new Error(`Unrecognized node type: ${node.type}\nNode: ${JSON.stringify(node)}\n`)
  }, '')
}

// serialize attributes object into a html attribute string
function attributes (options, obj) {
  return ' ' + _.reduce(obj, (m, v, k) => {
    // attribute values can be full trees, so we recurse here
    const resolvedAttr = walk(Array.prototype.concat(v), options)
    // if the value is truthy, we have key/val pair, if not, boolean
    m += resolvedAttr ? `${k}=\\"${resolvedAttr}\\" ` : `${k} `
    return m
  }, '').slice(0, -1)
}

// a very simple character escape
function escape (val) {
  const res = JSON.stringify(val)
  return res.substring(1, res.length - 1)
}

// in order for it to be possible to use function locals like '{{ foo }}'
// instead of '{{ locals.foo }}', they need to be wrapped with some extra code
// to make this possible, provided by the 'with' node module
function formatLocals (js, options) {
  const withWrap = addWith('locals || {}', `res = ${js}`, [options.runtimeName, 'res'])
  return `var res;${withWrap};return res`
}
